package com.anji.hyperneat.nd;

import java.math.BigInteger;
import java.util.Iterator;
import java.util.List;

/**
 * Provides support for N-Dimensional float array.
 * Underlying data structure is a 1D array, for fastest possible access. 
 * Only provides functionality for storing and retrieving values from the array;
 * doesn't provide support for any matrix type calculations. 
 * For speed reasons, gets/sets do not perform bounds checking and do not check that
 * you've specified a coordinate for each dimension.  
 * @author Shaun Lusk
 *
 */
public class NDFloatArray implements Cloneable {
	// The data
	private float m[];
	
	// Contains the size of each dimension; dimension.length = # of dimensions
	private final int[] dimensions;
	
	/* The pre-calculated "area" for counting by a dimension as it maps to a single dimensional array;
	 * e.g., for an XY space with D0=X and D1=Y, asize[0] = 1 (counting by x's)  and asize[1] = MaxVal(X).
	 */
	private final int[] asize;
	
	/**
	 * Creates an N-Dimensional Float matrix with the specified dimensions.
	 * @param dimensions A list or array specifying the size of each desired dimension;
	 * e.g., new NDFloatMatrix(5, 5) creates a 2D matrix with length and width = 5
	 */
	public NDFloatArray(int... dimensions) {
		this.dimensions = dimensions;
		this.asize = new int[dimensions.length];
		
		int size = dimensions[0];
		asize[0] = 1;
		for (int i = 1; i < dimensions.length; i++) {
			size *= dimensions[i];
			asize[i] = asize[i-1] * dimensions[i-1];
		}
		m = new float[size];
	}
	
	/**
	 * Creates an N-Dimensional Float matrix with the specified dimensions.
	 * @param dimensionsSet A list OF arrays specifying the size of each desired dimension;
	 * e.g., new NDFloatMatrix({5,5}, {6,6}, {7,7,8}) creates a 7D matrix 5x5x6x6x7x7x8
	 */
	public NDFloatArray(int[]... dimensionsSet) {
		int dimCount = 0;
		for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
			dimCount += dimensionsSet[dimSet].length;
		}
		
		this.dimensions = new int[dimCount];
		dimCount = 0;
		
		for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
			for (int dim = 0; dim < dimensionsSet[dimSet].length; dim++) {
				this.dimensions[dimCount++] = dimensionsSet[dimSet][dim];
			}
		}
		
		this.asize = new int[dimensions.length];
		
		int size = dimensions[0];
		asize[0] = 1;
		for (int i = 1; i < dimensions.length; i++) {
			size *= dimensions[i];
			asize[i] = asize[i-1] * dimensions[i-1];
		}
		m = new float[size];
	}
	
	/**
	 * Creates an N-Dimensional Float matrix with the specified dimensions, with each set cast to castSize
	 * @param dimensionsSet A list OF arrays specifying the size of each desired dimension;
	 * e.g., new NDFloatMatrix({5,5}, {6,6}, {7,7,8}) creates a 7D matrix 5x5x6x6x7x7x8
	 */
	public NDFloatArray(int castSize, int[]... dimensionsSet) {
		int dimCount;
		
		this.dimensions = new int[castSize * dimensionsSet.length];
		dimCount = 0;
		
		for (int dimSet = 0; dimSet < dimensionsSet.length; dimSet++) {
			for (int dim = 0; dim < castSize; dim++) {
				// Are we in the bounds of the originally specified dimension?
				if (dim < dimensionsSet[dimSet].length) {
					// If so, go ahead and use the index
					this.dimensions[dimCount++] = dimensionsSet[dimSet][dim];	
				} else {
					// otherwise, cast to size one
					this.dimensions[dimCount++] = 1;
				}
			}
		}
		
		this.asize = new int[dimensions.length];
		
		int size = dimensions[0];
		asize[0] = 1;
		for (int i = 1; i < dimensions.length; i++) {
			size *= dimensions[i];
			asize[i] = asize[i-1] * dimensions[i-1];
		}
		m = new float[size];
	}
	
	/**
	 * Private constructor for cloning.
	 * @param dimensions
	 * @param asize
	 * @param m
	 */
	private NDFloatArray(int[] dimensions, int[] asize, float[] m){
		this.dimensions = dimensions;
		this.asize = asize;
		this.m = m;
	}
	
	/**
	 * Sets all values in the array to 0. 
	 */
	public void clear() {
		for (int i = 0; i < m.length; i++) {
			m[i] = 0.0f;
		}
	}
	
	/**
	 * Compares the dimensions of this Array to another; returns true if they match, false otherwise.
	 * @param other The other array to compare against.
	 * @return
	 */
	public boolean dimensionsMatch(NDFloatArray other) {
		if (this.dimensions.length != other.dimensions.length)	return false;
		for (int i = 0; i < dimensions.length; i++) {
			if (this.dimensions[i] != other.dimensions[i]) return false;
		}
		return true;
	}
	
	
	/**
	 * Retrieves the float at the point specified by the coordinates;
	 * the number of coordinates must match the number of dimensions, or an
	 * exception will be thrown.
	 * E.g., get(3, 4, 5) retrieves the point of those coordinates in a 3D matrix. 
	 * @param coords The list or array of coordinates for the desired point.
	 * @return The value at the specified point in the matrix.
	 */
	public float get(int... coords) {
		int idx = 0;
		for (int d = dimensions.length-1; d >= 0; d--) {
			idx += asize[d] * coords[d];
		}
		return m[idx];
	}
	
	/**
	 * Sets the float value at the point specified by the coordinates;
	 * the number of coordinates must match the number of dimensions, or an
	 * exception will be thrown.
	 * E.g., get(5.0, 3, 4, 5) sets the point (3,4,5) in a 3D matrix to a value of 5.0.
	 * @param f The value to set. 
	 * @param coords The list or array of coordinates for the desired point.
	 */
	public void set(float f, int... coords) {
		int idx = 0;
		for (int d = dimensions.length-1; d >= 0; d--) {
			idx += asize[d] * coords[d];
		}
		
		m[idx] = f;
	}
	
	/**
	 * @return The number of dimensions this array has.
	 */
	public int getNumDimensions() {return dimensions.length;}
	
	/**
	 * @param dimension The dimension for which you wish to retrieve the size.
	 * @return The size of the specified dimension.
	 */
	public int getSizeOfDimension(int dimension) {return dimensions[dimension];}
	
	/**
	 * @return An int array describing the dimensions of this Array.
	 */
	public int[] getDimensions() {return dimensions;}
	
	/**
	 * Convenience method for returning the dimensions as a string in the format of #x#x#, 
	 * e.g., 
	 * 5x5x1
	 * @return The string representing the dimensions.
	 */
	public String getDimensionString() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < dimensions.length - 1; i++) {
			sb.append(dimensions[i]);
			sb.append("x");
		}
		sb.append(dimensions[dimensions.length-1]);
		
		return sb.toString();
	}
	
	/**
	 * @return The total size of the array.
	 */
	public int size() {return m.length;}
	
	/**
	 * Returns an iterator for the array;
	 * if you are stepping through the entire array, using the iterator will
	 * be faster than iterating using get().
	 * @return An array iterator.
	 */
	public MatrixIterator iterator() {return new MatrixIterator();}
	
	/**
	 * Retrives the coordinates of the element in this Array with the highest value.
	 * @return Coordinates of max value element.
	 */
    public int[] getCoordinatesOfMaxValue() {
        int[] coords = null;
        float best = -1;
        
        for (MatrixIterator it = this.iterator(); it.hasNext(); it.next()) {
        	if (null == coords || it.get() > best) {
        		best = it.get();
        		coords = it.getCurrentCoordinates();
        	}
        }
        return coords;
    }
	
    /**
     * Convenience method to retrieve a complete set of coordinates from two sets of coordinates.
     * @param coords1 Set of coordinates for the first node
     * @param coords2 set of coordinates for the second node
     * @return
     */
	public float get(int[] coords1, int[] coords2) {
		int[] coords = new int[coords1.length + coords2.length];
		for(int i = 0; i < coords1.length; i++) coords[i] = coords1[i];
		for(int i = 0; i < coords2.length; i++) coords[i+coords1.length] = coords2[i];
		return get(coords);
	}
	
    /**
     * Creates a clone of this Array.
     */
    public NDFloatArray clone() {
    	int[] odimensions = new int[dimensions.length];
    	System.arraycopy(dimensions, 0, odimensions, 0, dimensions.length);
    	
    	int[] oasize = new int[asize.length];
    	System.arraycopy(asize, 0, oasize, 0, asize.length);
    	
    	float[] om = new float[m.length]; 
    	System.arraycopy(m, 0, om, 0, m.length);

    	NDFloatArray oarray = new NDFloatArray(odimensions, oasize, om);
    	return oarray;
    }
    
    public void copyTo(NDFloatArray receiver) {
    	if (receiver.m.length != this.m.length || receiver.dimensions.length != this.dimensions.length )
    	
    	System.arraycopy(this.dimensions, 0, receiver.dimensions, 0, this.dimensions.length);
    	
    	System.arraycopy(this.asize, 0, receiver.asize, 0, this.asize.length);
    	 
    	System.arraycopy(this.m, 0, receiver.m, 0, this.m.length);

    }
    
    public float getFromRawCoordinate(int coord) {
    	return m[coord];
    }
    
	/**
	 * Builds a string representation of this array.
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder();
		if (dimensions.length == 1) {
			for (int i = 0; i < m.length; i++) sb.append(String.format("%1.3f", m[i]) + " ");
		} else if (dimensions.length == 2) {
			int idx = 0; 
			while (idx < m.length) {
				sb.append(String.format("%1.3f", m[idx++]) + " ");
				if (idx % dimensions[0] == 0) sb.append("\n"); 
					
			}
		} else {
			// On a 2d screen, this is about the best we can do:
			int idx = 0; 
			while (idx < m.length) {
				sb.append(String.format("%1.3f", m[idx++]) + " ");
				if (idx % dimensions[0] == 0) sb.append("\n"); 
				if (idx % (dimensions[0] * dimensions[1]) == 0) sb.append("\n");
			}
		}
		return sb.toString();
	}

	/**
	 * FOR TESTING ONLY
	 * @param args
	 */
	public static void main(String[] args) {

//		int xs = 3
//		, ys = 1
//		, zs = 1
////		, ws = 2
////		, ts = 1
//		;
//		int[] a = new int[] {xs, ys, zs /*ws, ts*/};
//		NDFloatArray fm = new NDFloatArray(a);
//		/*for (int t = 0; t < ts; t++) {
//			for (int w = 0; w < ws; w++) {*/
//				for (int z = 0; z < zs; z++) {
//					for (int y = 0; y < ys; y++) {
//						for (int x = 0; x < xs; x++) {
//							fm.set((float)(x+y+z /*+w+t*/), x, y, z /*, w, t*/);
//						}
//						
//					}
//				}
////			}
////		}
		int x = 2
		, y = 2
		, x1 = 3
		, y1 = 3
		, z = 4
		;
		int[] a = new int[] {x, x, x, x};
		int[] b = new int[] {y, x1};
		int[] c = new int[] {z, z, z+1};
		NDFloatArray fm = new NDFloatArray(4, a, b/*, c*/);
//		NDFloatArray q = new NDFloatArray(1, a, b);
		
//		for (int i = 0; i < fm.m.length; i++) System.out.print(String.format("%2.1f", fm.m[i]) + " ");
				
		System.out.println("\nend raw.");
		
		System.out.println("\n" + fm);
		
		System.out.println("numDim:" + fm.getNumDimensions());
		System.out.println("size:" + fm.size());
		
		for (int i = 0; i < fm.getNumDimensions(); i++) {
			System.out.println("\nDim:" + fm.getSizeOfDimension(i));
		}
		
//		System.out.println("Dump:");
		float r;
//		for (int t = 0; t < ts; t++) {
//			for (int w = 0; w < ws; w++) {
//				for (int z = 0; z < zs; z++) {
//					for (int y = 0; y < ys; y++) {
//						for (int x = 0; x < xs; x++) {
//							r = fm.get( x, y, z, w, t);
//							System.out.print(String.format("%2.0f", r) + " ");
//						}
//						System.out.println();
//					}
//					System.out.println();
//				}
//			}
//		}
		
		System.out.println("Dump2:");
		MatrixIterator it = fm.iterator();
		int[] c1;
		while (it.hasNext()) {
			c1 = it.getCurrentCoordinates();
			r = it.next();
//			System.out.print(String.format("%2.0f", r) + " ");
			for (int d = 0; d<c1.length; d++) System.out.print(c1[d] + ",");
			System.out.println();
		}

	}
	
	/**
	 * A simple iterator for the array;
	 * if you are stepping through the entire array, using the iterator will
	 * be faster than iterating using get().
	 *
	 */
	public class MatrixIterator implements Iterator {
		private int idx;
		private int[] c;
		
		private MatrixIterator() {
			idx = 0;
			c = new int[dimensions.length];
		}
		
		@Override
		public boolean hasNext() {return idx < m.length;}

		@Override
		public Float next() {
			return m[idx++];
		}

		@Override
		public void remove() {/* no-op*/}	
		
		public int[] getCurrentCoordinates() {
			 
			int localidx = idx;
			for (int dim = dimensions.length-1; dim >=0 ; dim--) {
				c[dim] = localidx / asize[dim];
				localidx -= asize[dim] * c[dim] ;
			}
			return c;
		}
		
		public int getRawCoordinate() {return idx;}
		
		public void set(float f) {m[idx] = f;}
		public float get() {return m[idx];}
		public void reset() {idx = 0;}
	}

}

